using System;
using System.Collections.Generic;
using System.Configuration;
using System.Linq;
using System.Linq.Expressions;
using MongoDB.Driver;
using MongoDB.Driver.Builders;

namespace MongoDB.Dynamic
{
    public static class Dynamic
    {
        #region class Dynamic

        private static string _defaultConnectionstringName = "MongoServerSettings";
        private static bool _notifyPropertyChanged;
        private static bool _audit;

        public static void Configure(string connStringOrName = "MongoServerSettings", bool notifyProperyChanged = false, bool audit = false)
        {
            if (!string.IsNullOrEmpty(connStringOrName))
                _defaultConnectionstringName = connStringOrName;

            _notifyPropertyChanged = notifyProperyChanged;
            _audit = audit;
        }

        public static MongoDatabase Db
        {
            get
            {
                var url = new MongoUrl(GetConnString());
                return MongoServer.Create(url).GetDatabase(url.DatabaseName);
            }
        }

        public static string GetConnString()
        {
            return ConfigurationManager.ConnectionStrings[_defaultConnectionstringName].ConnectionString;
        }

        private static MongoDatabase GetDatabaseFromUrl(MongoUrl url)
        {
            var server = MongoServer.Create(url.ToServerSettings());
            return server.GetDatabase(url.DatabaseName);
        }

        public static MongoCollection<T> GetCollectionFromConnectionString<T>(string connectionstring = null)
        {
            var connStr = connectionstring ?? GetConnString();
            return GetDatabaseFromUrl(new MongoUrl(connStr))
                .GetCollection<T>(GetCollectionName<T>());
        }

        public static MongoCollection<T> GetCollectionFromUrl<T>(MongoUrl url)
        {
            return GetDatabaseFromUrl(url)
                .GetCollection<T>(GetCollectionName<T>());
        }

        private static string GetCollectionName<T>()
        {
            return typeof(T).Name;
        }

        private static readonly Dictionary<string, object> Repositorories = new Dictionary<string, object>();

        public static DynamicCollection<T> GetCollection<T>() where T : class
        {
            var key = typeof(T).Name;
            object value;
            if (!Repositorories.TryGetValue(key, out value))
            {
                var repo = new DynamicCollection<T>(notifyPropertyChanged: _notifyPropertyChanged, audit: _audit);
                value = repo;
                Repositorories[key] = value;
                repo.PrepareEagerLoad();

            }
            return (DynamicCollection<T>)value;
        }

        internal static DynamicCollectionBase BuildRepository(Type type)
        {
            var mi = typeof(Dynamic).GetMethod("GetCollection");
            var constr = mi.GetGenericMethodDefinition().MakeGenericMethod(type);
            var result = constr.Invoke(null, null);
            return (DynamicCollectionBase)result;
        }

        #endregion

        public static class Config
        {
            #region members

            private static readonly Dictionary<Type, string> KeyNames
                = new Dictionary<Type, string>();
            private static readonly Dictionary<Type, List<Tuple<IMongoIndexKeys, IMongoIndexOptions>>> Indexes
                = new Dictionary<Type, List<Tuple<IMongoIndexKeys, IMongoIndexOptions>>>();

            private static readonly List<ForeignKeyDef> ForeignKeyDefs = new List<ForeignKeyDef>();
            private static readonly List<ChildCollectionDef> ChildCollectionDefs = new List<ChildCollectionDef>();

            #endregion

            #region GetKey/SetKey names

            /// <summary>
            /// Configure the property that will be used as Key in document
            /// </summary>
            public static void SetKeyName<TContract>(Expression<Func<TContract, object>> expression)
            {
                var memberName = expression.GetMemberName();
                KeyNames[typeof(TContract)] = memberName;
            }

            internal static bool TryGetKeyName<TContract>(out string keyName)
            {
                if (KeyNames.ContainsKey(typeof(TContract)))
                {
                    keyName = KeyNames[typeof(TContract)];
                    return true;
                }
                keyName = string.Empty;
                return false;
            }

            #endregion

            #region Get/Set Indexes

            /// <summary>
            /// Configures indexes that will be enforced during Dynamic Collection instantiation
            /// </summary>
            public static void SetIndex<TContract>(string indexName, bool isUnique, params Expression<Func<TContract, object>>[] expressions)
            {
                var fieldNames = expressions.Select(Utilities.GetMemberName).ToList();
                SetIndex<TContract>(indexName, isUnique, fieldNames.ToArray());
            }

            /// <summary>
            /// Configures indexes that will be enforced during Dynamic Collection instantiation
            /// </summary>
            public static void SetIndex<TContract>(string indexName, bool isUnique, params string[] fieldNames)
            {
                IMongoIndexKeys ib = new IndexKeysBuilder().Ascending(fieldNames);
                IMongoIndexOptions io = new IndexOptionsBuilder().SetUnique(isUnique).SetName(indexName);

                List<Tuple<IMongoIndexKeys, IMongoIndexOptions>> list;
                if (!Indexes.TryGetValue(typeof(TContract), out list))
                {
                    list = new List<Tuple<IMongoIndexKeys, IMongoIndexOptions>>();
                    Indexes[typeof(TContract)] = list;
                }
                list.Add(new Tuple<IMongoIndexKeys, IMongoIndexOptions>(ib, io));
            }

            internal static List<Tuple<IMongoIndexKeys, IMongoIndexOptions>> GetIndexes<TContract>()
            {
                List<Tuple<IMongoIndexKeys, IMongoIndexOptions>> list;
                return Indexes.TryGetValue(typeof(TContract), out list)
                    ? list
                    : new List<Tuple<IMongoIndexKeys, IMongoIndexOptions>>();
            }

            #endregion

            #region structs

            /// <summary>
            /// Struct used as storage for definitions of a child collection that will be eager loaded.
            /// </summary>
            internal struct ChildCollectionDef
            {
                public string MasterTable { get; set; }
                public string MasterProperty { get; set; }
                public string DetailTable { get; set; }
                public string DetailKey { get; set; }
            }

            /// <summary>
            /// Struct used as storage for definitions os FK's that will be eager loaded
            /// </summary>
            internal struct ForeignKeyDef
            {
                public string CurrentTable { get; set; }
                public string ComplexPropertyName { get; set; }
                public string SimpleProperty { get; set; }

                public string OtherTable { get; set; }
            }

            #endregion

            #region Configure EagerLoad

            public static void LoadCollection<T, T2>(Expression<Func<T, IEnumerable<T2>>> collectionExpr, Expression<Func<T2, object>> keyExpr)
            {
                var qcInfo = new ChildCollectionDef
                {
                    MasterTable = typeof(T).Name,
                    MasterProperty = collectionExpr.GetMemberName(),
                    DetailTable = typeof(T2).Name,
                    DetailKey = keyExpr.GetMemberName()
                };

                ChildCollectionDefs.Add(qcInfo);
            }

            public static void LoadFK<TTable, TOtherTable>(Expression<Func<TTable, TOtherTable>> complexProperty,
                Expression<Func<TTable, object>> simpleProperty)
            {
                var qInfo = new ForeignKeyDef
                {
                    CurrentTable = typeof(TTable).Name,
                    ComplexPropertyName = complexProperty.GetMemberName(),
                    OtherTable = typeof(TOtherTable).Name,
                    SimpleProperty = simpleProperty.GetMemberName()
                };

                ForeignKeyDefs.Add(qInfo);
            }

            internal static bool IsEagerLoadEnabled<TContract>()
            {
                var compareStr = typeof(TContract).Name;

                return ForeignKeyDefs.Any(q => q.CurrentTable == compareStr) || ChildCollectionDefs.Any(q => q.MasterTable == compareStr);
            }

            internal static ForeignKeyDef GetDefForFK<TContract>(string complexPropertyName)
            {
                return ForeignKeyDefs
                    .Where(q => q.CurrentTable == typeof(TContract).Name && q.ComplexPropertyName == complexPropertyName)
                    .FirstOrDefault();
            }

            internal static ChildCollectionDef GetDefForChildCollection<TContract>(string childCollection)
            {
                return ChildCollectionDefs.Where(q => q.MasterTable == typeof(TContract).Name &&
                    q.DetailTable == childCollection).FirstOrDefault();
            }

            #endregion

        }
    }
}